/*
 * Copyright 2007 Matt Jensen
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.jtell.spring;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;

/**
 * <p>
 * <code>ChannelBeanDefinitionParser</code> is an implementation of the Spring {@link BeanDefinitionParser} interface
 * which is used to create a JTell event channel when a <code>&lt;jtell:channel ...&gt;</code> element is used in a bean
 * definition file.
 * </p>
 * <p>
 * <strong>Thread Safety</strong><br/>
 * Instances of this class are safe for multithreaded access.
 * </p>
 */
public class ChannelBeanDefinitionParser extends AbstractBeanDefinitionParser
{
    private static Log LOG = LogFactory.getLog(ChannelBeanDefinitionParser.class);

    /**
     * <p>
     * Construct a {@link ChannelBeanDefinitionParser} instance.
     * </p>
     */
    public ChannelBeanDefinitionParser()
    {
        super();
    }

    /* inherit javadoc */
    protected AbstractBeanDefinition parseInternal(final Element element, final ParserContext parserContext)
    {
        // Get attributes and check their sanity.
        final String channelRef = element.getAttribute("parent-ref");
        final boolean gotChannelRef = StringUtils.hasText(channelRef);
        final String contextRef = element.getAttribute("context-ref");
        final boolean gotContextRef = StringUtils.hasText(contextRef);
        final String publishSpringEvents = element.getAttribute("publishSpringEvents");
        final String registerSpringBeans = element.getAttribute("registerSpringBeans");
        if (gotChannelRef && gotContextRef)
        {
            throw new IllegalStateException("The [parent-ref] and [context-ref] attributes cannot be used together.");
        }

        // Create the channel bean definition.
        final RootBeanDefinition channelDefinition = new RootBeanDefinition(EventChannelFactoryBean.class);
        if (gotChannelRef)
        {
            channelDefinition.getPropertyValues().addPropertyValue("parentChannel",
                    new RuntimeBeanReference(channelRef));
        }
        else if (gotContextRef)
        {
            channelDefinition.getPropertyValues().addPropertyValue("context", new RuntimeBeanReference(contextRef));
        }

        // If the publishSpringEvents attribute was set to "true," register a JTellApplicationEventMulticaster under the
        // appropriate name into the bean registry.
        final String id = element.getAttribute("id");
        final BeanDefinitionRegistry registry = parserContext.getRegistry();
        if (Boolean.valueOf(publishSpringEvents))
        {
            // Create the listener bean to publish Spring events on the event channel.
            // TODO: Is there a way to hook up the contextDefinition without resorting to reading the 'id' attribute off of the DOM element? Seems like there should be.
            final RootBeanDefinition publicationListenerDefinition =
                    new RootBeanDefinition(EventChannelPublicationListener.class);
            publicationListenerDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0,
                    new RuntimeBeanReference(id));

            // Register the bean definition under the name "applicationEventMulticaster" so that Spring recognizes it.
            registry.registerBeanDefinition(id + "_eventPublicationListener", publicationListenerDefinition);
        }

        // If the registerSpringBeans attribute was set to "true," register a BeanPostProcessor which will register all
        // Spring beans as listeners on the event channel.
        if (Boolean.valueOf(registerSpringBeans))
        {
            // Create the bean post processor definition.
            final RootBeanDefinition postProcessorDefinition =
                    new RootBeanDefinition(ListenerRegisteringBeanPostProcessor.class);
            final ConstructorArgumentValues constructorArgumentValues =
                    postProcessorDefinition.getConstructorArgumentValues();
            // TODO: Is there a way to hook up the contextDefinition without resorting to reading the 'id' attribute off of the DOM element? Seems like there should be.
            constructorArgumentValues.addIndexedArgumentValue(0, new RuntimeBeanReference(element.getAttribute("id")));

            // Register the post processor.
            registry.registerBeanDefinition(element.getAttribute("id") + ".postProcessor", postProcessorDefinition);
        }

        // Return the channel bean definition.
        return channelDefinition;
    }
}
